從今天開始,我們進入第四週:外掛開發與框架整合。前三週我們學會了畫各式各樣的圖表、串接真實資料、處理互動事件,也搞定了響應式設計,這些都是「使用 Chart.js 內建功能」就能完成的事。但實務上常常會遇到「內建功能做不到」的需求,例如:在圖表上疊加浮水印、在資料點旁邊直接顯示數字標籤、在圖表載入前顯示自訂的載入動畫……這些需求,都要透過 Chart.js 的**外掛系統(Plugin System)**來實現。今天要打好外掛系統的地基:了解 Plugin 的架構、
id與register的關係、如何安裝與設定官方外掛chartjs-plugin-datalabels,並整理 Chart.js 生命週期(Lifecycle)中所有可以「插隊」動手腳的 Hook。
先建立一個核心觀念:Chart.js 本身內建的功能(軸線、圖例、Tooltip 等),其實也是用「外掛」的方式實作的。也就是說,Plugin 系統不是一個附加的「外掛工具」,而是 Chart.js 整個渲染引擎的核心骨架。
官方文件是這樣描述的:
Plugins are the most efficient way to customize or change the default behavior of a chart.
翻成白話文:Plugin 是客製化或修改圖表預設行為最有效率的方式。與其在自己的程式碼裡「用暴力手法」直接操作 Canvas、修改 Chart.js 的內部資料,不如透過官方提供的 Plugin 介面,在指定的時間點「掛入」自己的邏輯,讓客製化的行為跟著 Chart.js 的渲染流程走,穩定又好維護。
最簡單的 Plugin,長得像這樣(一個普通的 JavaScript 物件):
const backgroundColorPlugin = {
id: 'customCanvasBackgroundColor', // 外掛的唯一識別碼
beforeDraw: (chart, args, options) => {
const { ctx } = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over'; // 畫在圖表內容「後面」
ctx.fillStyle = options.color || 'lightgreen';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
這段程式碼定義了一個外掛:在 Chart.js 每次繪製圖表之前(beforeDraw),先把整張畫布(Canvas)填滿一層背景色。有了這樣的架構,我們就能在 Chart.js 原本「畫座標軸 => 畫資料 => 畫圖例 => 畫 Tooltip」的固定流程中,插入任何自己想要的客製化行為。
只想讓某一個圖表套用這個外掛,透過建立圖表時的 plugins 陣列傳入:
const plugin = { /* 外掛實作內容 */ };
// chart1 與 chart2 都會套用這個 plugin
const chart1 = new Chart(ctx1, {
type: 'bar',
data: data1,
plugins: [plugin]
});
const chart2 = new Chart(ctx2, {
type: 'line',
data: data2,
plugins: [plugin]
});
// chart3 沒有傳入 plugins,所以不會套用
const chart3 = new Chart(ctx3, { type: 'pie', data: data3 });
這種寫法的特點是:外掛可以在多個圖表之間共用(傳入同一個物件即可),但只有明確寫在 plugins 陣列裡的圖表才會套用。
如果專案中每一張圖表都要套用同一個外掛(例如:所有圖表都要有浮水印),一個一個手動加入 plugins 陣列會很麻煩。這時可以用 Chart.register() 把外掛註冊成全域外掛:
Chart.register({
id: 'customCanvasBackgroundColor',
beforeDraw: (chart, args, options) => { /* ... */ }
});
// 之後建立的所有圖表都會自動套用這個外掛,不需要再傳 plugins
const chart = new Chart(ctx, { type: 'bar', data });
如果只是想「臨時」加一段客製化邏輯,不打算重複使用,可以直接把外掛物件寫在 plugins 陣列裡,不用另外宣告變數:
const chart = new Chart(ctx, {
type: 'bar',
data,
plugins: [{
beforeInit: (chart, args, options) => {
console.log('圖表即將初始化!');
}
}]
});
⚠️ 重要限制:Inline Plugin 不會被註冊(registered),也因此無法被全域註冊使用,也不能透過
options.plugins.{id}設定選項(因為它通常連id都沒有)。有些外掛(例如需要被其他外掛引用、或需要支援options設定的外掛)就不能用 Inline 的方式使用,必須乖乖走 Per-chart 或 Global 註冊。
id、register、options.plugins 三角關係id:外掛的身分證字號想要讓外掛「可以被設定(Configurable)」,就一定要給它一個獨一無二的 id。
Chart.register():把外掛「掛」進 Chart.jsregister 是把外掛(或控制器、座標軸等元件)正式登記到 Chart.js 內部的動作。只有註冊過的外掛,才能:
options.plugins.{id} 設定選項(就算是 Per-chart 傳入,也需要先有 id 才能被設定,不一定要 register,但沒有 register 就無法「全域套用」)。import { Chart, registerables } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
Chart.register(...registerables, ChartDataLabels); // 一次註冊完整功能 + datalabels 外掛
options.plugins:外掛專屬的設定區每個外掛的設定選項,會放在 options.plugins.{外掛的 id} 底下,彼此互不干擾:
const chart = new Chart(ctx, {
type: 'bar',
data,
options: {
plugins: {
legend: { display: true }, // 內建 legend 外掛的設定
tooltip: { enabled: true }, // 內建 tooltip 外掛的設定
datalabels: { // chartjs-plugin-datalabels 的設定(id 就是 'datalabels')
color: '#36454f',
anchor: 'end',
align: 'top'
}
}
}
});
// 只停用 id 為 'datalabels' 的外掛(僅針對這個圖表實例)
options: {
plugins: {
datalabels: false
}
}
// 停用「這個圖表」的所有外掛(包含內建的 legend、tooltip 等)
options: {
plugins: false
}
這個機制非常實用:例如專案中大部分圖表都要顯示資料標籤,但某一張特殊的圖表(例如資料點太密集)不想顯示標籤,就可以只針對那一張圖表把 datalabels: false 關掉,不需要動到全域設定。
外掛也可以在物件裡定義 defaults,讓使用者不設定時也有合理的預設行為:
const plugin = {
id: 'custom_canvas_background_color',
beforeDraw: (chart, args, options) => {
const { ctx } = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = options.color;
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
},
defaults: {
color: 'lightGreen' // 使用者沒有設定 options.plugins.custom_canvas_background_color.color 時的預設值
}
};
chartjs-plugin-datalabelschartjs-plugin-datalabels 是 Chart.js 生態系中最受歡迎的官方外掛之一,功能是直接在資料點(圓餅圖的扇形、長條圖的長條、折線圖的節點)旁邊顯示數值標籤,不需要滑鼠 Hover 就能看到具體數字,非常適合報表或簡報情境。
透過 CDN(適合單純用 <script> 引入的專案):
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
透過 CDN 引入時,chartjs-plugin-datalabels 會建立全域變數 window.ChartDataLabels,但不會自動註冊——官方從 1.x 版開始就取消了自動註冊的行為,無論是 CDN 或 npm 安裝,都必須手動呼叫 Chart.register(ChartDataLabels) 外掛才會真正生效。
透過 npm(適合搭配 Vite / Webpack 等打包工具的專案):
npm install chart.js@4.5.1 chartjs-plugin-datalabels@2.2.0
import { Chart, registerables } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
Chart.register(...registerables, ChartDataLabels); // 需要手動 register,CDN 引入方式也不例外
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['一月', '二月', '三月', '四月'],
datasets: [{
label: '銷售額(萬元)',
data: [65, 59, 80, 81],
backgroundColor: '#4bc0c0'
}]
},
options: {
plugins: {
datalabels: {
color: '#333', // 標籤文字顏色
anchor: 'end', // 標籤錨點位置:資料點的哪個位置
align: 'top', // 標籤相對錨點的對齊方式
font: { weight: 'bold' },
formatter: (value) => `${value} 萬` // 自訂顯示格式
}
}
}
});
anchor:決定標籤要「錨定」在資料元素(長條、扇形、節點)的哪個位置,常見值有 'center'、'start'、'end'。align:標籤相對於錨點「往哪個方向」對齊,例如 'top'(往上)、'bottom'(往下)。formatter:一個函式,可以把原始數值轉換成任何顯示格式(例如加上千分位、百分比符號、幣別文字)。options: {
plugins: {
datalabels: {
display: (context) => context.dataset.data[context.dataIndex] > 50 // 只顯示大於 50 的標籤
}
}
}
或是針對「不需要標籤的那張圖」直接關閉:
options: {
plugins: {
datalabels: false
}
}
plugins: [{...}] 裡的物件)沒有經過 Chart.register(),無法被其他圖表共用,也通常無法透過 options.plugins.{id} 設定選項。如果外掛需要被多張圖表重複使用,或需要支援選項設定,請改用 Per-chart 或 Global 的方式。id 重複或缺漏:如果外掛沒有設定 id,或是跟其他已註冊的外掛 id 撞名,會導致設定選項對應錯亂,甚至互相覆蓋彼此的邏輯。afterDraw,會蓋住原本已經畫好的資料內容;反之把「浮水印」畫在 beforeDraw,則會被之後畫的資料內容蓋住。畫面的視覺順序,就是 Hook 被呼叫的順序。ctx.save() / ctx.restore():外掛在 Hook 裡直接操作 Canvas 的 2D Context(例如改變 fillStyle、globalCompositeOperation)時,若沒有用 ctx.save() 保存狀態、事後用 ctx.restore() 還原,可能會影響到 Chart.js 接下來自己繪製圖表元素時使用的樣式設定。Chart.register(plugin) 只是讓外掛「可以被使用」(並且如果沒有特別停用,會被所有圖表套用),跟 Chart.defaults 全域樣式設定(Day 24 會介紹)是兩個不同的機制,不要混為一談。chartjs-plugin-datalabels 導致外掛沒生效:不論是透過 CDN(<script> 引入後得到全域變數 ChartDataLabels)還是 npm(import ChartDataLabels from 'chartjs-plugin-datalabels'),從 1.x 版開始都不會自動註冊,必須自己手動呼叫 Chart.register(ChartDataLabels),否則外掛不會生效,畫面上完全看不到資料標籤也不會有錯誤訊息,很容易花時間排查。明天(Day 23)我們會延續今天建立的 Hook 觀念,正式動手撰寫屬於自己的外掛:從最簡單的自訂背景色開始,實作「浮水印」與「圖表正中央標籤」兩個實用小外掛,並深入了解外掛的 options 該如何設計,讓外掛在 Global(全域)與 Per-chart(單一圖表)兩種情境下,都能有各自獨立、互不干擾的設定。